feat(ai): AI gateway integration with custom provider support #1072
feat(ai): AI gateway integration with custom provider support #1072ddecrulle wants to merge 5 commits into
Conversation
|
📝 WalkthroughWalkthroughAdds full AI gateway integration to Onyxia: a new ChangesAI Gateway Integration
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
web/src/core/ports/Ai.ts (1)
8-11: ⚡ Quick win
errorvariant carries no diagnostic payload — error context is lostThe
{ status: "error" }branch gives thunks and UI no information about what went wrong (network failure, unexpected HTTP status, etc.), making it hard to display a meaningful error message. Even a minimalreason?: stringorhttpStatus?: numberwould allow the UI to differentiate transient failures from configuration errors.♻️ Proposed extension
export type GetTokenResult = | { status: "success"; token: string } | { status: "no-account" } - | { status: "error" }; + | { status: "error"; reason?: string };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/core/ports/Ai.ts` around lines 8 - 11, The GetTokenResult union currently has an { status: "error" } variant with no diagnostic data; update the type so the "error" branch carries minimal payload (e.g., reason?: string and httpStatus?: number or similar fields) so callers can surface meaningful messages; change the union definition for GetTokenResult to include those optional fields and then update all places that construct or pattern-match on GetTokenResult (e.g., any code creating a { status: "error" } result or switching on GetTokenResult.status) to populate and consume the new fields.web/src/core/ports/OnyxiaApi/XOnyxia.ts (1)
196-203: 💤 Low value
enabled: truediscriminant is redundant given| undefinedSince
aiis typed as{ enabled: true; ... } | undefined, theenabled: trueliteral adds no information — an AI block being present already implies it is enabled. Chart templates that checkai.enabledcould equivalently checkai != null. Consider dropping theenabledfield to keep the shape consistent with howvaultands3are modelled in the same type.♻️ Proposed simplification
ai: | { - enabled: true; token: string; apiBase: string; model: string; } | undefined;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts` around lines 196 - 203, Remove the redundant enabled: true literal from the ai union type in XOnyxia (i.e., change the ai type from "{ enabled: true; token: string; apiBase: string; model: string; } | undefined" to "{ token: string; apiBase: string; model: string; } | undefined") and update any runtime checks that test ai.enabled to instead test for ai != null (or Boolean(ai)), ensuring places referencing the removed enabled property (e.g., code paths that read ai.enabled) are adjusted to treat presence/absence of the ai object as the enablement signal.web/src/core/adapters/ai/openWebUi.ts (1)
40-43: ⚡ Quick winUnguarded cast on
data.datamay throw aTypeErrorat runtime.If the gateway returns a response where
data.dataisundefined,null, or not an array,.map()will throw, which callers don't expect —listModelsshould either validate or provide a fallback.🛡️ Proposed defensive guard
const data = await response.json(); -return (data.data as { id: string }[]).map(m => m.id); +const entries: unknown[] = Array.isArray(data.data) ? data.data : []; +return (entries as { id: string }[]).map(m => m.id);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/core/adapters/ai/openWebUi.ts` around lines 40 - 43, The unguarded cast in listModels reads response.json() into data and assumes data.data is an array, which can throw when it's undefined/null/not-an-array; update the listModels implementation to validate that data && Array.isArray(data.data) before mapping, and if not return an empty array (or a safe fallback). When mapping, defensively extract ids (e.g., filter items with an id property and coerce to string) so the method always returns string[] without throwing.web/src/core/usecases/ai/thunks.ts (1)
145-150: 🏗️ Heavy lift
testCustomProviderreturns aPromise<string[]>which violates the reactive thunk pattern.Per the coding guidelines for
web/src/core/**/*.ts: "Use observable state with thunks (reactive pattern) rather than promise-based returns; dispatch actions and read state instead of returning values from thunks."The test result (model list or error) should be stored in state via new actions (e.g.,
providerTestStarted,providerTestSucceeded({ models }),providerTestFailed) and exposed via a selector, with the UI subscribing to state instead of awaiting the thunk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/core/usecases/ai/thunks.ts` around lines 145 - 150, testCustomProvider currently returns a Promise<string[]> which breaks the project's reactive thunk pattern; change it to dispatch actions and update state instead of returning values. Replace the direct return of fetchModels(apiBase, apiKey) in testCustomProvider with dispatch(providerTestStarted()), call fetchModels, then on success dispatch(providerTestSucceeded({ models })) and on error dispatch(providerTestFailed({ error })); update the reducer/state to store test status and models and expose them via a selector for the UI to subscribe to, and make the thunk return void (or Promise<void>) rather than Promise<string[]>; reference the existing testCustomProvider thunk and fetchModels helper when applying these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/src/core/usecases/ai/selectors.ts`:
- Around line 16-26: Replace the hard assertion in the "no-account" branch so
the selector won't throw when region.ai is missing: remove the assert on
region.ai in the branch where initializationStatus === "no-account" and return
isEnabled: false and initializationStatus as before but only attach webUiUrl
when region.ai exists (e.g., use a safe optional access or conditional property)
so the selector returns a safe fallback instead of throwing; look for symbols
initializationStatus, region.ai and the returned webUiUrl in this function to
update.
In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 190-193: The call to ai.listModels(token) can throw and leaves the
initializeStart/“pending” state unresolved; wrap the ai.listModels(token)
invocation in a try/catch around the block that follows initializeStart and, on
any error, dispatch initializeFailed with the error (or a normalized message) so
the thunk resolves correctly; reference tokenResult, ai.listModels(token),
initializeStart, and initializeFailed when locating where to add the try/catch
and the error dispatch.
In `@web/src/core/usecases/launcher/thunks.ts`:
- Around line 777-790: The current ai block builds an ai object even when
aiState.selectedModel is undefined (using selectedModel ?? ""), which injects an
empty model into the XOnyxiaContext; update the anonymous IIFE in the ai
property (the code using aiUsecase.selectors.main(getState()) and aiState) to
return undefined unless aiState.isEnabled, aiState.token are present AND
aiState.selectedModel is a non-empty value (e.g., check aiState.selectedModel !=
null && aiState.selectedModel !== ""), otherwise return undefined so no ai key
is written into the context.
In `@web/src/ui/i18n/resources/no.tsx`:
- Around line 126-127: Update the localized helper string for the translation
key "custom providers section helper" to include the user-facing disclosure that
credentials (API keys) are stored locally in the browser; locate the string for
"custom providers section helper" in web/src/ui/i18n/resources/no.tsx and modify
its value to append a short sentence making the storage disclosure (e.g.,
"API-nøkler lagres i nettleseren"), keeping the rest of the guidance about base
URL and API key unchanged.
In `@web/src/ui/pages/account/AccountAiTab.tsx`:
- Around line 100-112: Wrap the ai.addCustomProvider call inside onSaveProvider
in a try/catch so failures are handled: call ai.addCustomProvider(...) in the
try block and only clear the form (setAddFormOpen(false), setPendingLabel(""),
setPendingApiBase(""), setPendingApiKey(""), setTestStatus("idle"),
setTestModelCount(0)) on success; in catch set a failure state (e.g.,
setTestStatus("error") and optionally set an error message state) and keep the
add form open so the user can correct inputs; ensure the catch re-enables any UI
affordances disabled during save and does not leave the promise rejection
unhandled.
In `@web/src/ui/pages/account/Page.tsx`:
- Line 56: The ternary in the .filter callback for accountTabId is redundant;
replace the ternary that returns true when accountTabId !== "ai" else
ai.isAvailable() with a logical OR of the inequality check and ai.isAvailable()
(i.e., test whether accountTabId is not "ai" OR ai.isAvailable()) in the Page
component where accountTabId and ai.isAvailable() are used, and apply the same
simplification to the sibling .filter calls that use the same pattern.
---
Nitpick comments:
In `@web/src/core/adapters/ai/openWebUi.ts`:
- Around line 40-43: The unguarded cast in listModels reads response.json() into
data and assumes data.data is an array, which can throw when it's
undefined/null/not-an-array; update the listModels implementation to validate
that data && Array.isArray(data.data) before mapping, and if not return an empty
array (or a safe fallback). When mapping, defensively extract ids (e.g., filter
items with an id property and coerce to string) so the method always returns
string[] without throwing.
In `@web/src/core/ports/Ai.ts`:
- Around line 8-11: The GetTokenResult union currently has an { status: "error"
} variant with no diagnostic data; update the type so the "error" branch carries
minimal payload (e.g., reason?: string and httpStatus?: number or similar
fields) so callers can surface meaningful messages; change the union definition
for GetTokenResult to include those optional fields and then update all places
that construct or pattern-match on GetTokenResult (e.g., any code creating a {
status: "error" } result or switching on GetTokenResult.status) to populate and
consume the new fields.
In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts`:
- Around line 196-203: Remove the redundant enabled: true literal from the ai
union type in XOnyxia (i.e., change the ai type from "{ enabled: true; token:
string; apiBase: string; model: string; } | undefined" to "{ token: string;
apiBase: string; model: string; } | undefined") and update any runtime checks
that test ai.enabled to instead test for ai != null (or Boolean(ai)), ensuring
places referencing the removed enabled property (e.g., code paths that read
ai.enabled) are adjusted to treat presence/absence of the ai object as the
enablement signal.
In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 145-150: testCustomProvider currently returns a Promise<string[]>
which breaks the project's reactive thunk pattern; change it to dispatch actions
and update state instead of returning values. Replace the direct return of
fetchModels(apiBase, apiKey) in testCustomProvider with
dispatch(providerTestStarted()), call fetchModels, then on success
dispatch(providerTestSucceeded({ models })) and on error
dispatch(providerTestFailed({ error })); update the reducer/state to store test
status and models and expose them via a selector for the UI to subscribe to, and
make the thunk return void (or Promise<void>) rather than Promise<string[]>;
reference the existing testCustomProvider thunk and fetchModels helper when
applying these changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2cb9147a-b5ad-46ea-be5c-b123ddd634a2
📒 Files selected for processing (30)
web/CLAUDE.mdweb/src/core/adapters/ai/index.tsweb/src/core/adapters/ai/mock.tsweb/src/core/adapters/ai/openWebUi.tsweb/src/core/adapters/onyxiaApi/ApiTypes.tsweb/src/core/adapters/onyxiaApi/onyxiaApi.tsweb/src/core/bootstrap.tsweb/src/core/ports/Ai.tsweb/src/core/ports/OnyxiaApi/DeploymentRegion.tsweb/src/core/ports/OnyxiaApi/XOnyxia.tsweb/src/core/tools/oidcTokenExchange.tsweb/src/core/usecases/ai/index.tsweb/src/core/usecases/ai/selectors.tsweb/src/core/usecases/ai/state.tsweb/src/core/usecases/ai/thunks.tsweb/src/core/usecases/index.tsweb/src/core/usecases/launcher/thunks.tsweb/src/ui/i18n/resources/de.tsxweb/src/ui/i18n/resources/en.tsxweb/src/ui/i18n/resources/es.tsxweb/src/ui/i18n/resources/fi.tsxweb/src/ui/i18n/resources/fr.tsxweb/src/ui/i18n/resources/it.tsxweb/src/ui/i18n/resources/nl.tsxweb/src/ui/i18n/resources/no.tsxweb/src/ui/i18n/resources/zh-CN.tsxweb/src/ui/i18n/types.tsweb/src/ui/pages/account/AccountAiTab.tsxweb/src/ui/pages/account/Page.tsxweb/src/ui/pages/account/accountTabIds.ts
| "custom providers section helper": | ||
| "Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.", |
There was a problem hiding this comment.
Keep API key storage disclosure in localized helper text.
This helper omits that credentials are stored in the browser, which is important user-facing security context.
Suggested wording patch
- "custom providers section helper":
- "Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",
+ "custom providers section helper":
+ "Legg til dine egne AI-leverandører (OpenAI, Anthropic eller andre OpenAI-kompatible endepunkter). Legitimasjon lagres i nettleseren din.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "custom providers section helper": | |
| "Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.", | |
| "custom providers section helper": | |
| "Legg til dine egne AI-leverandører (OpenAI, Anthropic eller andre OpenAI-kompatible endepunkter). Legitimasjon lagres i nettleseren din.", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/ui/i18n/resources/no.tsx` around lines 126 - 127, Update the
localized helper string for the translation key "custom providers section
helper" to include the user-facing disclosure that credentials (API keys) are
stored locally in the browser; locate the string for "custom providers section
helper" in web/src/ui/i18n/resources/no.tsx and modify its value to append a
short sentence making the storage disclosure (e.g., "API-nøkler lagres i
nettleseren"), keeping the rest of the guidance about base URL and API key
unchanged.
| const onSaveProvider = useConstCallback(async () => { | ||
| await ai.addCustomProvider({ | ||
| label: pendingLabel, | ||
| apiBase: pendingApiBase, | ||
| apiKey: pendingApiKey | ||
| }); | ||
| setAddFormOpen(false); | ||
| setPendingLabel(""); | ||
| setPendingApiBase(""); | ||
| setPendingApiKey(""); | ||
| setTestStatus("idle"); | ||
| setTestModelCount(0); | ||
| }); |
There was a problem hiding this comment.
Handle add-provider failures in onSaveProvider.
If ai.addCustomProvider fails at Line 101, the rejection is unhandled and the UI gives no clear recovery path. Catch the error and keep the form in a controlled error state.
Proposed fix
const onSaveProvider = useConstCallback(async () => {
- await ai.addCustomProvider({
- label: pendingLabel,
- apiBase: pendingApiBase,
- apiKey: pendingApiKey
- });
+ try {
+ await ai.addCustomProvider({
+ label: pendingLabel,
+ apiBase: pendingApiBase,
+ apiKey: pendingApiKey
+ });
+ } catch {
+ setTestStatus("error");
+ return;
+ }
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onSaveProvider = useConstCallback(async () => { | |
| await ai.addCustomProvider({ | |
| label: pendingLabel, | |
| apiBase: pendingApiBase, | |
| apiKey: pendingApiKey | |
| }); | |
| setAddFormOpen(false); | |
| setPendingLabel(""); | |
| setPendingApiBase(""); | |
| setPendingApiKey(""); | |
| setTestStatus("idle"); | |
| setTestModelCount(0); | |
| }); | |
| const onSaveProvider = useConstCallback(async () => { | |
| try { | |
| await ai.addCustomProvider({ | |
| label: pendingLabel, | |
| apiBase: pendingApiBase, | |
| apiKey: pendingApiKey | |
| }); | |
| } catch { | |
| setTestStatus("error"); | |
| return; | |
| } | |
| setAddFormOpen(false); | |
| setPendingLabel(""); | |
| setPendingApiBase(""); | |
| setPendingApiKey(""); | |
| setTestStatus("idle"); | |
| setTestModelCount(0); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/ui/pages/account/AccountAiTab.tsx` around lines 100 - 112, Wrap the
ai.addCustomProvider call inside onSaveProvider in a try/catch so failures are
handled: call ai.addCustomProvider(...) in the try block and only clear the form
(setAddFormOpen(false), setPendingLabel(""), setPendingApiBase(""),
setPendingApiKey(""), setTestStatus("idle"), setTestModelCount(0)) on success;
in catch set a failure state (e.g., setTestStatus("error") and optionally set an
error message state) and keep the add form open so the user can correct inputs;
ensure the catch re-enables any UI affordances disabled during save and does not
leave the promise rejection unhandled.
| .filter(accountTabId => | ||
| accountTabId !== "vault" ? true : vaultCredentials.isAvailable() | ||
| ) | ||
| .filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable())) |
There was a problem hiding this comment.
Simplify the negated ternary condition (SonarCloud warning).
accountTabId !== "ai" ? true : ai.isAvailable() reads as a double-negation and SonarCloud flags it. The direct form is clearer:
♻️ Proposed simplification
-.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))
+.filter(accountTabId => accountTabId !== "ai" || ai.isAvailable())Note: the same pattern appears in the sibling .filter calls on lines 46, 49–52, and 53–55 — those could be cleaned up consistently in a follow-up.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable())) | |
| .filter(accountTabId => accountTabId !== "ai" || ai.isAvailable()) |
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis
[warning] 56-56: Unexpected negated condition.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/ui/pages/account/Page.tsx` at line 56, The ternary in the .filter
callback for accountTabId is redundant; replace the ternary that returns true
when accountTabId !== "ai" else ai.isAvailable() with a logical OR of the
inequality check and ai.isAvailable() (i.e., test whether accountTabId is not
"ai" OR ai.isAvailable()) in the Page component where accountTabId and
ai.isAvailable() are used, and apply the same simplification to the sibling
.filter calls that use the same pattern.
- Add AI usecase (state/thunks/selectors) with initializeStart/initializeSucceed/initializeFailed lifecycle actions - getToken() returns a discriminated result type in the Ai port — no-account (403) vs error cases handled without leaking adapter details into usecases - Gracefully disable AI features on init failure; show a link to the gateway URL when user has no account - Add AccountAiGatewayTab with token/model display and full i18n for all 9 languages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
web/src/core/ports/OnyxiaApi/XOnyxia.ts (1)
185-194: ⚡ Quick winPrefer optional property syntax for
aicontext.
ai: {...} | undefinedforces callers to always include the key. If this field is truly optional,ai?: {...}is the safer contract for existingXOnyxiaContextobject construction paths.Suggested change
- ai: - | { + ai?: + { enabled: boolean; apiKey: string; apiBase: string; model: string; provider: string; embeddingsModel: string; } - | undefined; + ;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts` around lines 185 - 194, The ai property in the XOnyxiaContext type uses union syntax with undefined (ai: {...} | undefined) which requires callers to always provide the key. Replace this with the optional property syntax (ai?: {...}) to allow the property to be omitted entirely when constructing XOnyxiaContext objects, making it a safer contract for existing code paths that build this type.web/src/core/ports/Ai.ts (1)
1-8: ⚡ Quick winUse an interface for the port contract in
src/core/ports.This contract should be declared as an
interfaceto match the ports-and-adapters rule forweb/src/core/ports/**/*.ts.Suggested change
-export type Ai = { +export interface Ai { id: string; name: string; webUiUrl: string; apiBase: string; getToken: () => Promise<GetTokenResult>; listModels: (token: string) => Promise<{ id: string; name: string }[]>; -}; +}As per coding guidelines,
web/src/core/ports/**/*.ts: “Define TypeScript interfaces for external dependencies in src/core/ports/ using the ports-and-adapters pattern.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/core/ports/Ai.ts` around lines 1 - 8, The Ai export in the file should be declared as an interface instead of a type alias to follow the ports-and-adapters coding guidelines. Replace the existing type Ai declaration (which uses the type keyword with an object literal syntax) with an interface declaration that has the same name and properties, removing the equals sign and curly brace syntax used for type aliases.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/src/core/adapters/ai/mock.ts`:
- Line 10: The apiBase string composition concatenates webUiUrl directly with
`/api`, which creates a double slash (`...//api`) if webUiUrl ends with a
forward slash, potentially breaking strict gateways or proxies. Normalize the
webUiUrl by removing any trailing slash before composing the apiBase string. Use
a method to strip the trailing slash from webUiUrl (such as using replace or
trimEnd with proper regex/string manipulation) so that the final apiBase always
has exactly one slash between the base URL and the `/api` path.
In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 31-42: The Zod schema in the fetchModels function requires a name
field that does not exist in the actual OpenAI API response, causing parse
failures. Modify the schema object definition to either make the name field
optional by adding .optional() to the z.string() for name, or use
.default(model.id) to derive the name from the id field as a fallback. If using
the optional approach, update the mapping logic to handle cases where name may
be undefined. If using the default approach, ensure the default value is applied
appropriately in the schema definition.
In `@web/src/core/usecases/launcher/thunks.ts`:
- Line 772: The activeProvider selector can return a valid AI context object
with model set to an empty string when no model is selected, which causes issues
with AI-enabled Helm charts. At the location where
aiUsecase.selectors.activeProvider is called in the launcher thunks, either add
validation logic in the launcher UI to prevent service launch when modelId is
undefined (enforcing model selection before launch), or modify the
activeProvider selector itself to return undefined instead of a valid context
object when provider.selection.modelId is not set, ensuring downstream code
receives undefined rather than a context with an empty model string.
---
Nitpick comments:
In `@web/src/core/ports/Ai.ts`:
- Around line 1-8: The Ai export in the file should be declared as an interface
instead of a type alias to follow the ports-and-adapters coding guidelines.
Replace the existing type Ai declaration (which uses the type keyword with an
object literal syntax) with an interface declaration that has the same name and
properties, removing the equals sign and curly brace syntax used for type
aliases.
In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts`:
- Around line 185-194: The ai property in the XOnyxiaContext type uses union
syntax with undefined (ai: {...} | undefined) which requires callers to always
provide the key. Replace this with the optional property syntax (ai?: {...}) to
allow the property to be omitted entirely when constructing XOnyxiaContext
objects, making it a safer contract for existing code paths that build this
type.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 374834a3-0605-4579-b5f9-554819d05232
📒 Files selected for processing (32)
web/CLAUDE.mdweb/src/core/adapters/ai/index.tsweb/src/core/adapters/ai/mock.tsweb/src/core/adapters/ai/openWebUi.tsweb/src/core/adapters/onyxiaApi/ApiTypes.tsweb/src/core/adapters/onyxiaApi/onyxiaApi.tsweb/src/core/bootstrap.tsweb/src/core/ports/Ai.tsweb/src/core/ports/OnyxiaApi/DeploymentRegion.tsweb/src/core/ports/OnyxiaApi/XOnyxia.tsweb/src/core/tools/oidcTokenExchange.tsweb/src/core/usecases/ai/decoupledLogic/persistedAiConfig.tsweb/src/core/usecases/ai/index.tsweb/src/core/usecases/ai/selectors.tsweb/src/core/usecases/ai/state.tsweb/src/core/usecases/ai/thunks.tsweb/src/core/usecases/index.tsweb/src/core/usecases/launcher/thunks.tsweb/src/core/usecases/userConfigs.tsweb/src/ui/i18n/resources/de.tsxweb/src/ui/i18n/resources/en.tsxweb/src/ui/i18n/resources/es.tsxweb/src/ui/i18n/resources/fi.tsxweb/src/ui/i18n/resources/fr.tsxweb/src/ui/i18n/resources/it.tsxweb/src/ui/i18n/resources/nl.tsxweb/src/ui/i18n/resources/no.tsxweb/src/ui/i18n/resources/zh-CN.tsxweb/src/ui/i18n/types.tsweb/src/ui/pages/account/AccountAiTab.tsxweb/src/ui/pages/account/Page.tsxweb/src/ui/pages/account/accountTabIds.ts
💤 Files with no reviewable changes (13)
- web/src/ui/i18n/resources/en.tsx
- web/src/ui/pages/account/accountTabIds.ts
- web/src/ui/i18n/resources/fr.tsx
- web/src/ui/i18n/resources/zh-CN.tsx
- web/src/ui/i18n/types.ts
- web/src/ui/i18n/resources/it.tsx
- web/src/core/usecases/ai/index.ts
- web/src/ui/i18n/resources/nl.tsx
- web/src/ui/i18n/resources/no.tsx
- web/src/ui/i18n/resources/es.tsx
- web/src/ui/pages/account/Page.tsx
- web/src/ui/i18n/resources/fi.tsx
- web/src/ui/pages/account/AccountAiTab.tsx
✅ Files skipped from review due to trivial changes (1)
- web/CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (7)
- web/src/core/adapters/ai/index.ts
- web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
- web/src/core/usecases/index.ts
- web/src/core/adapters/onyxiaApi/ApiTypes.ts
- web/src/ui/i18n/resources/de.tsx
- web/src/core/tools/oidcTokenExchange.ts
- web/src/core/adapters/ai/openWebUi.ts
| id, | ||
| name, | ||
| webUiUrl, | ||
| apiBase: `${webUiUrl}/api`, |
There was a problem hiding this comment.
Normalize webUiUrl before composing apiBase.
If webUiUrl ends with /, this produces a double slash (...//api), which can break strict gateways/proxies.
Suggested change
export function createAi(params: { id: string; name: string; webUiUrl: string }): Ai {
const { id, name, webUiUrl } = params;
+ const normalizedWebUiUrl = webUiUrl.replace(/\/+$/, "");
return {
id,
name,
webUiUrl,
- apiBase: `${webUiUrl}/api`,
+ apiBase: `${normalizedWebUiUrl}/api`,
getToken: async () => ({ status: "success" as const, token: "mock-ai-token" }),
listModels: async () => [📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| apiBase: `${webUiUrl}/api`, | |
| export function createAi(params: { id: string; name: string; webUiUrl: string }): Ai { | |
| const { id, name, webUiUrl } = params; | |
| const normalizedWebUiUrl = webUiUrl.replace(/\/+$/, ""); | |
| return { | |
| id, | |
| name, | |
| webUiUrl, | |
| apiBase: `${normalizedWebUiUrl}/api`, | |
| getToken: async () => ({ status: "success" as const, token: "mock-ai-token" }), | |
| listModels: async () => [ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/core/adapters/ai/mock.ts` at line 10, The apiBase string composition
concatenates webUiUrl directly with `/api`, which creates a double slash
(`...//api`) if webUiUrl ends with a forward slash, potentially breaking strict
gateways or proxies. Normalize the webUiUrl by removing any trailing slash
before composing the apiBase string. Use a method to strip the trailing slash
from webUiUrl (such as using replace or trimEnd with proper regex/string
manipulation) so that the final apiBase always has exactly one slash between the
base URL and the `/api` path.
| async function fetchModels(apiBase: string, apiKey: string): Promise<AiModel[]> { | ||
| const response = await fetch(`${apiBase}/models`, { | ||
| headers: { Authorization: `Bearer ${apiKey}` } | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch models (${response.status})`); | ||
| } | ||
| const { data } = z | ||
| .object({ data: z.array(z.object({ id: z.string(), name: z.string() })) }) | ||
| .parse(await response.json()); | ||
| return data.map(({ id, name }) => ({ id, name })); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
OpenAI API /models endpoint response schema
💡 Result:
The OpenAI API GET /models endpoint returns a list of available models. Its response follows a standard schema containing a top-level object and an array of model entities [1][2]. Response Schema: { "object": "list", "data": [ { "id": "model-id", "object": "model", "created": 1686935002, "owned_by": "openai" } ] } Key Fields: 1. object (string): The type of the top-level object, which is always "list" [1][2]. 2. data (array): A list of model objects [1][2]. Model Object Fields: * id (string): The unique identifier for the model, used in other API endpoints [1][2][3]. * object (string): The type of the object, which is always "model" [1][2][4]. * created (integer): The Unix timestamp (in seconds) representing when the model was created [1][2][3]. * owned_by (string): The name of the organization that owns the model (e.g., "openai") [1][2][3].
Citations:
- 1: https://openai-hd4n6.mintlify.app/api-reference/models/list-models
- 2: https://www.withone.ai/knowledge/openai/conn_mod_def%3A%3AGJ566eWSXhI%3A%3AAceRWldHQp2BdGJXAPXjEQ
- 3: https://developers.openai.com/api/reference/ruby/resources/models/
- 4: https://app.unpkg.com/openai@6.26.0/files/resources/models.d.mts
Fix Zod schema to match actual OpenAI API response format.
The schema at lines 31-42 requires a name field on each model object, but the OpenAI API /models endpoint response includes only id, object, created, and owned_by. The required name field will cause .parse() to fail on real API calls. Either:
- Make
nameoptional with.optional()and handle the missing field in the mapping, or - Derive
namefromidas a fallback (e.g.,.default(model.id))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/core/usecases/ai/thunks.ts` around lines 31 - 42, The Zod schema in
the fetchModels function requires a name field that does not exist in the actual
OpenAI API response, causing parse failures. Modify the schema object definition
to either make the name field optional by adding .optional() to the z.string()
for name, or use .default(model.id) to derive the name from the id field as a
fallback. If using the optional approach, update the mapping logic to handle
cases where name may be undefined. If using the default approach, ensure the
default value is applied appropriately in the schema definition.
| useCertManager: region.certManager?.useCertManager, | ||
| certManagerClusterIssuer: region.certManager?.certManagerClusterIssuer | ||
| }, | ||
| ai: aiUsecase.selectors.activeProvider(getState()), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if the UI enforces model selection before allowing service launch
rg -n "model.*select|selectedModel|modelId" --type=tsx web/src/ui/pages/account/Repository: InseeFrLab/onyxia
Length of output: 89
🏁 Script executed:
#!/bin/bash
# First, locate the AI usecase files
fd "ai" web/src/core/usecases/ --type f -name "*.ts" | head -20Repository: InseeFrLab/onyxia
Length of output: 292
🏁 Script executed:
#!/bin/bash
# Search for model selection in UI files with correct syntax
rg -n "model.*select|selectedModel|modelId" web/src/ui/ -A 2 -B 2 | head -100Repository: InseeFrLab/onyxia
Length of output: 3540
🏁 Script executed:
#!/bin/bash
# Check the AI usecase selectors.ts to understand activeProvider implementation
fd -i "selectors.ts" web/src/core/usecases/ai/Repository: InseeFrLab/onyxia
Length of output: 99
🏁 Script executed:
#!/bin/bash
# Read the AI usecase selectors to understand activeProvider
cat web/src/core/usecases/ai/selectors.tsRepository: InseeFrLab/onyxia
Length of output: 2076
🏁 Script executed:
#!/bin/bash
# Check the launcher thunks around line 772 to see context usage
sed -n '760,780p' web/src/core/usecases/launcher/thunks.tsRepository: InseeFrLab/onyxia
Length of output: 1038
🏁 Script executed:
#!/bin/bash
# Check launcher UI for model selection validation
rg "model|selectedModel|activeProvider" web/src/ui/pages/LaunchPad/ -A 2 -B 2 | head -80Repository: InseeFrLab/onyxia
Length of output: 129
🏁 Script executed:
#!/bin/bash
# Check if there's validation in launcher thunks before using context
sed -n '1,50p' web/src/core/usecases/launcher/thunks.ts | grep -A 5 -B 5 "activeProvider\|ai\."Repository: InseeFrLab/onyxia
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Find launcher-related UI files
fd -i "launch" web/src/ui/ --type fRepository: InseeFrLab/onyxia
Length of output: 274
🏁 Script executed:
#!/bin/bash
# Check if any validation happens in launcher thunks for model selection
rg "model|ai\." web/src/core/usecases/launcher/thunks.ts | head -30Repository: InseeFrLab/onyxia
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Check launcher main card for model selection
cat web/src/ui/pages/launcher/LauncherMainCard.tsxRepository: InseeFrLab/onyxia
Length of output: 22161
🏁 Script executed:
#!/bin/bash
# Search for where ai context is used, specifically model field
rg "\.model|ai\.model" web/src/ --type ts --type tsx -B 2 -A 2 | head -100Repository: InseeFrLab/onyxia
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Search for where XOnyxiaContext is used, particularly in Helm rendering
rg "XOnyxiaContext" web/src/ -A 3 -B 1 | head -60Repository: InseeFrLab/onyxia
Length of output: 4380
🏁 Script executed:
#!/bin/bash
# Look for Helm template context or variable injection related to ai.model
rg "ai\\.model|aiUsecase" web/src/core/usecases/launcher/ -A 2 -B 2Repository: InseeFrLab/onyxia
Length of output: 1108
🏁 Script executed:
#!/bin/bash
# Check if Helm schema validation would catch an empty model
rg "ai\\.model|model.*required|required.*model" web/src/core/usecases/launcher/ -A 2 -B 2Repository: InseeFrLab/onyxia
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Look for XOnyxiaContext type definition to understand ai field structure
fd "OnyxiaApi" web/src/core/ports/ --type fRepository: InseeFrLab/onyxia
Length of output: 103
🏁 Script executed:
#!/bin/bash
# Read OnyxiaApi to understand XOnyxiaContext and ai field structure
cat web/src/core/ports/OnyxiaApi/OnyxiaApi.ts | head -150Repository: InseeFrLab/onyxia
Length of output: 3152
🏁 Script executed:
#!/bin/bash
# Search for XOnyxiaContext type definition
rg "type XOnyxiaContext|interface XOnyxiaContext" web/src/core/ports/ -A 30Repository: InseeFrLab/onyxia
Length of output: 2300
🏁 Script executed:
#!/bin/bash
# Get more of the XOnyxiaContext type definition, particularly the ai field
rg "type XOnyxiaContext" web/src/core/ports/OnyxiaApi/XOnyxia.ts -A 100 | grep -A 50 "ai:"Repository: InseeFrLab/onyxia
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Read the full XOnyxia.ts file to see the ai field definition
cat web/src/core/ports/OnyxiaApi/XOnyxia.ts | tail -200Repository: InseeFrLab/onyxia
Length of output: 6364
🏁 Script executed:
#!/bin/bash
# Check if launcher prevents launching without required fields or validates ai.model
rg "ai\.model|model.*required|required.*model|validate.*ai|getXOnyxiaContext" web/src/core/usecases/launcher/thunks.ts -B 3 -A 3 | head -80Repository: InseeFrLab/onyxia
Length of output: 1134
🏁 Script executed:
#!/bin/bash
# Check if there's any Helm schema validation for model field in launcher
rg "helmValuesSchema|JSONSchema|required|validation" web/src/core/usecases/launcher/decoupledLogic.ts | head -30Repository: InseeFrLab/onyxia
Length of output: 150
🏁 Script executed:
#!/bin/bash
# Find Helm value computation logic
fd "decoupled\|compute" web/src/core/usecases/launcher/ --type fRepository: InseeFrLab/onyxia
Length of output: 43
🏁 Script executed:
#!/bin/bash
# Check the full getXOnyxiaContext function to see if model validation happens
sed -n '690,800p' web/src/core/usecases/launcher/thunks.tsRepository: InseeFrLab/onyxia
Length of output: 4925
activeProvider selector can return model: "" when no model is selected.
The aiUsecase.selectors.activeProvider returns a valid AI context even when provider.selection.modelId is undefined (defaulting to empty string). The launcher UI does not enforce model selection before launch, so Helm templates can receive ai.model: "", which may cause issues with AI-enabled charts that expect a non-empty model value.
Consider either:
- Enforcing model selection in the launcher UI before allowing service launch, or
- Adjusting the selector to return
undefinedwhenmodelIdis not set
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/core/usecases/launcher/thunks.ts` at line 772, The activeProvider
selector can return a valid AI context object with model set to an empty string
when no model is selected, which causes issues with AI-enabled Helm charts. At
the location where aiUsecase.selectors.activeProvider is called in the launcher
thunks, either add validation logic in the launcher UI to prevent service launch
when modelId is undefined (enforcing model selection before launch), or modify
the activeProvider selector itself to return undefined instead of a valid
context object when provider.selection.modelId is not set, ensuring downstream
code receives undefined rather than a context with an empty model string.



Aiport (OIDC token exchange, model listing) with an OpenWebUI adapter and a mock; config comes from the deployment region(
region.ai)aiusecase — initialize lifecycle (pending/success/no-account/error), token refresh, model selection; selected model is injected into Helm values when launching aservice
localStorage
/models), shows success with model count or an error messageSummary by CodeRabbit